﻿Imports System.Windows.Shapes
Imports System.Windows.Media
Imports System.Windows.Media.Animation

''' <summary>
''' The StatusLED class implements the control for an on-screen LED status "light".
''' The LED it controls can be any .NET Shape object.  The primary function of
''' the class is to manage the visual representation of the LED according to its 
''' current Status and Power values.  
''' 
''' The LED status can be any of: Okay, Warning or Error. 
''' The LED Power can be either: On or Off.
''' 
''' Color Chart accoring to Status and Power settings:
''' 
''' Status       Okay      Warning     Error
''' ----------------------------------------
''' Power ON     Green     Yellow      Red
''' Power OFF    Gray      Gray        Gray
''' 
''' The LED visual color state transitions are managed through .NET 
''' Storyboard animations.
''' 
''' </summary>
''' <remarks>The class is thread-safe with regards to UI updates as it dispatches 
''' UI updates associated with visual LED changes to the UIElement's Dispatcher. </remarks>
Public Class StatusLED
    ''' <summary>
    ''' Available LED Status Values
    ''' </summary>
    ''' <remarks>Visually: Okay=Green, Warning=Yellow, Err=Red</remarks>
    Public Enum StatusCode
        Okay = 0
        Warning = 1
        Err = 2
    End Enum

    ' -------------------------------------------------------------------------
    ' Constants (Logical)
    ' -------------------------------------------------------------------------
    ' Not sure how to declare Objects as a formal Const in VB.NET :-(
    Private COLOR_OFF As Color = System.Windows.Media.Color.FromRgb(35, 35, 35)
    Private COLOR_OKAY As Color = Colors.Green
    Private COLOR_WARN As Color = Colors.Yellow
    Private COLOR_ERROR As Color = Colors.Red

    ' -------------------------------------------------------------------------
    ' Member Variables
    ' -------------------------------------------------------------------------
    '
    Private mStatus As StatusCode = StatusCode.Okay
    Private mLedElement As Rectangle = Nothing
    Private mStoryboardOff As Storyboard = New Storyboard()
    Private mStoryboardOkay As Storyboard = New Storyboard()
    Private mStoryboardWarn As Storyboard = New Storyboard()
    Private mStoryboardError As Storyboard = New Storyboard()
    Private mIsOn As Boolean = False


    ' -------------------------------------------------------------------------
    ' Property Definitions
    ' -------------------------------------------------------------------------
    '
    ''' <summary>
    ''' Read-Only Property to retrieve the LED UIElement associated with this LED.
    ''' </summary>
    ''' <remarks>Cannot use a write property to set the UIElement as we need to 
    ''' pass the UIElement ByRef and VB.NET allows only ByVal passing of arguments 
    ''' to set properties.</remarks>
    Public ReadOnly Property UIElement As Shape
        Get
            Return mLedElement
        End Get
    End Property

    ''' <summary>
    ''' Method to set the UIElement asswociated with this LED.
    ''' </summary>
    ''' <param name="value">Any WPF Shape object actings as an on-screen LED.</param>
    ''' <remarks>Cannot use a write property to set the UIElement as we need to 
    ''' pass the UIElement ByRef and VB.NET allows only ByVal passing of arguments 
    ''' to set properties.</remarks>
    Public Sub setUIElement(ByRef value As Shape)
        If mLedElement Is Nothing Then
            If Not value Is Nothing Then
                mLedElement = value
                configureAnimation()
                dispatchUpdate()
            Else
                Throw New ArgumentException("Null UIElement cannot be used.")
            End If
        Else
            Throw New AccessViolationException("StatusLED.UIElement is a set-once Property.")
        End If

    End Sub

    Public ReadOnly Property IsOn As Boolean
        Get
            Return mIsOn
        End Get
    End Property

    ''' <summary>
    ''' Sets/Gets the current status of the LED.
    ''' </summary>
    ''' <remarks>Animations associated with Status changes are dispatched immediately to UI
    ''' if the Power state is ON.</remarks>
    Public Property Status As StatusCode
        Get
            Return mStatus
        End Get
        Set(ByVal value As StatusCode)
            If mStatus <> value Then
                mStatus = value
                If Me.IsOn Then
                    dispatchUpdate()
                End If
            End If
        End Set
    End Property


    ' -------------------------------------------------------------------------
    ' Main Code
    ' -------------------------------------------------------------------------
    '
    ''' <summary>
    ''' Default Constructor for delayed association with LED UIElement.
    ''' </summary>
    Public Sub New()
    End Sub

    ''' <summary>
    ''' Constructor to create StatusLED when UIElement is known.
    ''' </summary>
    ''' <param name="uiElement">Any WPF Shape Object</param>
    Public Sub New(ByRef uiElement As Shape)
        mLedElement = uiElement
        If Not mLedElement Is Nothing Then
            configureAnimation()
        End If

    End Sub

    ''' <summary>
    ''' Set Power state to OFF. Visually set to OFF color (Gray) if currently ON.
    ''' </summary>
    Public Sub turnLedOff()
        If Me.IsOn Then
            mIsOn = False
            dispatchUpdate()
        End If

    End Sub

    ''' <summary>
    ''' Set Power state to ON. Visually set to status color if currently OFF.
    ''' </summary>
    Public Sub turnLedOn()
        If Not Me.IsOn Then
            mIsOn = True
            dispatchUpdate()
        End If

    End Sub

    ''' <summary>
    ''' Invokes the Storyboard animations associated with the current LED status.
    ''' </summary>
    ''' <remarks>This method is ultimately called by the Dispatcher Thread of the UI Element
    ''' associated with StatusLED object and not necessarily the same thread invoking the upstream
    ''' Public methods of the class.</remarks>
    Private Sub updateLED()
        If Me.IsOn Then
            Select Case mStatus
                Case StatusCode.Err
                    mStoryboardError.Begin(Me.UIElement)
                Case StatusCode.Okay
                    mStoryboardOkay.Begin(Me.UIElement)
                Case StatusCode.Warning
                    mStoryboardWarn.Begin(Me.UIElement)
                Case Else
                    Throw New ArgumentOutOfRangeException("Unknown Status Code Provided: " & mStatus)
            End Select
        Else
            mStoryboardOff.Begin(Me.UIElement)
        End If
    End Sub

    ''' <summary>
    ''' Configures the LEd UIElement with Storyboard animations for changing its color.
    ''' </summary>
    Private Sub configureAnimation()

        ' All color LED color animations are performed against a Brush object that is associated
        ' with the Fill porperty of the UIElement.
        ' Create the Brush object and set to the OFF Color by default.
        Dim myAnimatedBrush As New SolidColorBrush()
        myAnimatedBrush.Color = COLOR_OFF
        mLedElement.Fill = myAnimatedBrush

        ' Create a Name Scope for the UIElement
        NameScope.SetNameScope(mLedElement, New NameScope())

        ' Register the brush's name with the LED UIElement Name Scope
        ' so that it can be targeted by storyboards. Append the object hashcode
        ' as a String to ensure a unique name.
        Dim brushName As String = "LEDAnimatedBrush_" & Me.GetHashCode.ToString
        mLedElement.RegisterName(brushName, myAnimatedBrush)

        ' Now install the indidivual state COLOR Storyboards
        installStoryboard(mStoryboardOff, COLOR_OFF, brushName)
        installStoryboard(mStoryboardOkay, COLOR_OKAY, brushName)
        installStoryboard(mStoryboardWarn, COLOR_WARN, brushName)
        installStoryboard(mStoryboardError, COLOR_ERROR, brushName)

    End Sub

    ''' <summary>
    ''' Installs/Configures one Storyboard animation based on a single color transition
    ''' </summary>
    ''' <param name="board">The Storyboard to add color animation to.</param>
    ''' <param name="color">The Color to transtion the Brush object to.</param>
    ''' <param name="brushName">The registered name of the Brush object to apply animation to.</param>
    ''' <remarks></remarks>
    Private Sub installStoryboard(ByRef board As Storyboard, ByRef color As Color, ByVal brushName As String)

        Dim ledColorAnimation As New ColorAnimation()

        ' Animation is to transition to target color from whatever color is currently showing.
        ledColorAnimation.To = color
        ' Perform the color transition in 0.15 seconds.
        ledColorAnimation.Duration = TimeSpan.FromSeconds(0.15)

        ' Setup animation targets (object and property)
        Storyboard.SetTargetName(ledColorAnimation, brushName)
        Storyboard.SetTargetProperty(ledColorAnimation, New PropertyPath(SolidColorBrush.ColorProperty))

        ' Finally add the color animation object to the the provided Storyboard.
        board.Children.Add(ledColorAnimation)

    End Sub

    ''' <summary>
    ''' Provides a thread-safe way to invoke UI changes to the LED UIElement regardless
    ''' of the calling thread.
    ''' </summary>
    ''' <remarks>UI Change is dispatched with a high priority.</remarks>
    Private Sub dispatchUpdate()
        If Not IsNothing(Me.UIElement) Then
            Me.UIElement.Dispatcher.
                BeginInvoke(New Action(AddressOf updateLED),
                            System.Windows.Threading.DispatcherPriority.Send)
        End If
    End Sub

End Class
